package com.gushikustudios.rube;
import java.util.HashMap;
import java.util.Map;
import com.badlogic.gdx.ApplicationListener;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.InputProcessor;
import com.badlogic.gdx.assets.AssetManager;
import com.badlogic.gdx.assets.loaders.resolvers.InternalFileHandleResolver;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.Texture.TextureWrap;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.PolygonRegion;
import com.badlogic.gdx.graphics.g2d.PolygonSpriteBatch;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.math.EarClippingTriangulator;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.physics.box2d.Body;
import com.badlogic.gdx.physics.box2d.BodyDef.BodyType;
import com.badlogic.gdx.physics.box2d.Box2DDebugRenderer;
import com.badlogic.gdx.physics.box2d.CircleShape;
import com.badlogic.gdx.physics.box2d.Contact;
import com.badlogic.gdx.physics.box2d.ContactImpulse;
import com.badlogic.gdx.physics.box2d.ContactListener;
import com.badlogic.gdx.physics.box2d.Fixture;
import com.badlogic.gdx.physics.box2d.Manifold;
import com.badlogic.gdx.physics.box2d.PolygonShape;
import com.badlogic.gdx.physics.box2d.Shape;
import com.badlogic.gdx.physics.box2d.World;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.GdxRuntimeException;
import com.gushikustudios.rube.loader.RubeSceneAsyncLoader;
import com.gushikustudios.rube.loader.RubeSceneLoader;
import com.gushikustudios.rube.loader.serializers.utils.RubeImage;
/**
* Use the left-click to pan. Scroll-wheel zooms.
*
* @author cvayer, tescott
*
*/
public class RubeLoaderTest implements ApplicationListener, InputProcessor, ContactListener
{
private OrthographicCamera mCam;
private OrthographicCamera mTextCam;
private RubeScene mScene;
private Box2DDebugRenderer mDebugRender;
private Array<SimpleSpatial> mSpatials; // used for rendering rube images
private Array<PolySpatial> mPolySpatials;
private Map<String, Texture> mTextureMap;
private Map<Texture, TextureRegion> mTextureRegionMap;
private static final Vector2 mTmp = new Vector2(); // shared by all objects
private static final Vector2 mTmp3 = new Vector2(); // shared during polygon creation
private SpriteBatch mBatch;
private PolygonSpriteBatch mPolyBatch;
private AssetManager mAssetManager;
// used for pan and scanning with the mouse.
private Vector3 mCamPos;
private Vector3 mCurrentPos;
private World mWorld;
private float mAccumulator; // time accumulator to fix the physics step.
private int mVelocityIter = 8;
private int mPositionIter = 3;
private float mSecondsPerStep = 1 / 60f;
private float mFlashLoadingText;
private boolean mHideLoadingText;
private static final float MAX_DELTA_TIME = 0.25f;
private BitmapFont mBitmapFont;
private static final String [][] RUBE_SCENE_FILE_LIST =
{
{
"data/palm.json"
},
{
"data/base.json",
"data/images1.json",
"data/bodies1.json",
"data/images2.json",
"data/bodies2.json",
"data/images3.json"
}
};
private static final float FLASH_RATE = 0.25f;
private enum GAME_STATE
{
STARTING,
LOADING,
RUNNING
};
private GAME_STATE mState;
@SuppressWarnings("unused")
private GAME_STATE mPrevState;
private GAME_STATE mNextState;
private boolean mUseAssetManager;
private int mRubeFileList;
private int mRubeFileIndex;
public RubeLoaderTest()
{
this(false);
}
public RubeLoaderTest(boolean useAssetManager)
{
mUseAssetManager = useAssetManager;
}
public RubeLoaderTest(boolean useAssetManager, int rubeFileList)
{
this(useAssetManager);
mRubeFileList = rubeFileList;
}
@Override
public void create()
{
float w = Gdx.graphics.getWidth();
float h = Gdx.graphics.getHeight();
mBitmapFont = new BitmapFont(Gdx.files.internal("data/arial-15.fnt"), false);
Gdx.input.setInputProcessor(this);
mCamPos = new Vector3();
mCurrentPos = new Vector3();
mCam = new OrthographicCamera(100, 100 * h / w);
mCam.position.set(50, 50, 0);
mCam.zoom = 1.8f;
mCam.update();
mTextCam = new OrthographicCamera(Gdx.graphics.getWidth(),Gdx.graphics.getHeight());
mTextCam.position.set(Gdx.graphics.getWidth()/2,Gdx.graphics.getHeight()/2,0);
mTextCam.zoom = 1;
mTextCam.update();
mDebugRender = new Box2DDebugRenderer();
mBatch = new SpriteBatch();
mPolyBatch = new PolygonSpriteBatch();
mTextureMap = new HashMap<String, Texture>();
mTextureRegionMap = new HashMap<Texture, TextureRegion>();
mState = mNextState = GAME_STATE.STARTING;
}
@Override
public void dispose()
{
if (mBatch != null)
{
mBatch.dispose();
}
if (mPolyBatch != null)
{
mPolyBatch.dispose();
}
if (mDebugRender != null)
{
mDebugRender.dispose();
}
if (mWorld != null)
{
mWorld.dispose();
}
if (mAssetManager != null)
{
mAssetManager.dispose();
}
}
@Override
public void render()
{
float delta = Gdx.graphics.getDeltaTime();
// cap maximum delta time...
if (delta > MAX_DELTA_TIME)
{
delta = MAX_DELTA_TIME;
}
update(delta);
present(delta);
// state transitions here...
mPrevState = mState;
mState = mNextState;
}
/**
* This method is used for game logic updates.
*
* @param delta
*/
private void update(float delta)
{
// game logic here...
mFlashLoadingText += delta;
if (mFlashLoadingText > FLASH_RATE)
{
mFlashLoadingText = 0;
mHideLoadingText = !mHideLoadingText;
}
switch (mState)
{
case STARTING:
initiateSceneLoad();
break;
case LOADING:
processSceneLoad();
break;
case RUNNING:
updatePhysics(delta);
break;
}
}
/**
* The present() method is for drawing / rendering...
*
* @param delta
*/
private void present(float delta)
{
// game rendering logic here...
Gdx.gl.glClearColor(0, 0, 0, 1);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
switch (mState)
{
case STARTING:
case LOADING:
if (!mHideLoadingText)
{
mBatch.setProjectionMatrix(mTextCam.combined);
mBatch.begin();
mBitmapFont.draw(mBatch,"Loading...",10,40);
mBatch.end();
}
break;
case RUNNING:
renderWorld(delta);
break;
}
}
/**
* Kicks off asset manager if selected...
*
*/
private void initiateSceneLoad()
{
if (mUseAssetManager)
{
// kick off asset manager operations...
mAssetManager = new AssetManager();
mAssetManager.setLoader(RubeScene.class, new RubeSceneAsyncLoader(new InternalFileHandleResolver()));
// kick things off..
mAssetManager.load(RUBE_SCENE_FILE_LIST[mRubeFileList][mRubeFileIndex], RubeScene.class);
}
mNextState = GAME_STATE.LOADING;
}
/**
* Either performs a blocking load or a poll on the asset manager load...
*/
private void processSceneLoad()
{
if (mAssetManager == null)
{
// perform a blocking load...
RubeSceneLoader loader = new RubeSceneLoader();
for (int i = 0; i < RUBE_SCENE_FILE_LIST[mRubeFileList].length; i++)
{
// each iteration adds to the scene that is ultimately returned...
mScene = loader.addScene(Gdx.files.internal(RUBE_SCENE_FILE_LIST[mRubeFileList][mRubeFileIndex++]));
}
processScene();
mNextState = GAME_STATE.RUNNING;
}
else if (mAssetManager.update())
{
// each iteration adds to the scene that is ultimately returned...
mScene = mAssetManager.get(RUBE_SCENE_FILE_LIST[mRubeFileList][mRubeFileIndex++], RubeScene.class);
if (mRubeFileIndex < RUBE_SCENE_FILE_LIST[mRubeFileList].length)
{
mAssetManager.load(RUBE_SCENE_FILE_LIST[mRubeFileList][mRubeFileIndex], RubeScene.class);
}
else
{
processScene();
mNextState = GAME_STATE.RUNNING;
}
}
}
/**
* Builds up world based on info from the scene...
*/
private void processScene()
{
createSpatialsFromRubeImages(mScene);
createPolySpatialsFromRubeFixtures(mScene);
mWorld = mScene.getWorld();
// configure simulation settings
mVelocityIter = mScene.velocityIterations;
mPositionIter = mScene.positionIterations;
if (mScene.stepsPerSecond != 0)
{
mSecondsPerStep = 1f / mScene.stepsPerSecond;
}
mWorld.setContactListener(this);
//
// example of custom property handling
//
Array<Body> bodies = mScene.getBodies();
if ((bodies != null) && (bodies.size > 0))
{
for (int i = 0; i < bodies.size; i++)
{
Body body = bodies.get(i);
String gameInfo = (String)mScene.getCustom(body, "GameInfo", null);
if (gameInfo != null)
{
System.out.println("GameInfo custom property: " + gameInfo);
}
}
}
// Example of accessing data based on name
System.out.println("body0 count: " + mScene.getNamed(Body.class, "body0").size);
System.out.println("fixture0 count: " + mScene.getNamed(Fixture.class, "fixture0").size);
mScene.printStats();
testSceneSettings();
mScene.clear(); // no longer need any scene references
}
/**
* Use an accumulator to ensure a fixed delta for physics simulation...
*
* @param delta
*/
private void updatePhysics(float delta)
{
mAccumulator += delta;
while (mAccumulator >= mSecondsPerStep)
{
mWorld.step(mSecondsPerStep, mVelocityIter, mPositionIter);
mAccumulator -= mSecondsPerStep;
}
}
/**
* Perform all world rendering...
*
* @param delta
*/
private void renderWorld(float delta)
{
if ((mSpatials != null) && (mSpatials.size > 0))
{
mBatch.setProjectionMatrix(mCam.combined);
mBatch.begin();
for (int i = 0; i < mSpatials.size; i++)
{
mSpatials.get(i).render(mBatch, 0);
}
mBatch.end();
}
if ((mPolySpatials != null) && (mPolySpatials.size > 0))
{
mPolyBatch.setProjectionMatrix(mCam.combined);
mPolyBatch.begin();
for (int i = 0; i < mPolySpatials.size; i++)
{
mPolySpatials.get(i).render(mPolyBatch, 0);
}
mPolyBatch.end();
}
mBatch.setProjectionMatrix(mTextCam.combined);
mBatch.begin();
mBitmapFont.draw(mBatch,"fps: " + Gdx.graphics.getFramesPerSecond(),10,20);
mBatch.end();
mDebugRender.render(mWorld, mCam.combined);
}
/**
* Validation on custom settings in the RUBE file.
*
*/
private void testSceneSettings()
{
//
// validate the custom settings attached to world object..
//
boolean testBool = (Boolean)mScene.getCustom(mWorld, "testCustomBool", false);
int testInt = (Integer)mScene.getCustom(mWorld, "testCustomInt", 0);
float testFloat = (Float)mScene.getCustom(mWorld, "testCustomFloat", 0);
Color color = (Color)mScene.getCustom(mWorld, "testCustomColor", null);
Vector2 vec = (Vector2)mScene.getCustom(mWorld, "testCustomVec2", null);
String string = (String)mScene.getCustom(mWorld, "testCustomString", null);
int bodies1Custom = (Integer)mScene.getCustom(mWorld, "bodies1Custom", 0);
int bodies2Custom = (Integer)mScene.getCustom(mWorld, "bodies2Custom", 0);
int bodiesCommonCustom = (Integer)mScene.getCustom(mWorld,"bodiesCommonCustom",0);
// validate multiple file reading...
if (mRubeFileList == 1)
{
if (bodies1Custom != 1)
{
throw new GdxRuntimeException("bodies1Custom not read correctly! Expected: " + 1 + " Actual: " + bodies1Custom);
}
if (bodies2Custom != 2)
{
throw new GdxRuntimeException("bodies2Custom not read correctly! Expected: " + 2 + " Actual: " + bodies2Custom);
}
// this is common between two files, but the last value will hold...
if (bodiesCommonCustom != 4321)
{
throw new GdxRuntimeException("bodiesCommonCustom not read correctly! Expected: " + 4321 + " Actual: " + bodiesCommonCustom);
}
System.out.println("Multiple file testing: PASSED!");
}
if (testBool == false)
{
throw new GdxRuntimeException("testCustomBool not read correctly! Expected: " + true + " Actual: " + testBool);
}
if (testInt != 8675309)
{
throw new GdxRuntimeException("testCustomInt not read correctly! Expected: " + 8675309 + " Actual: " + testInt);
}
if (testFloat != 1.25f)
{
throw new GdxRuntimeException("testCustomFloat not read correctly! Expected: " + 1.25f + " Actual: " + testFloat);
}
if (color == null)
{
throw new GdxRuntimeException("testCustomColor is reporting null!");
}
if ((color.r != 17f/255) || (color.g != 29f/255) || (color.b != 43f/255) || (color.a != 61f/255))
{
throw new GdxRuntimeException("testCustomColor not read correctly! Expected: " + new Color(17f/255,29f/255,43f/255,61f/255) + " Actual: " + color);
}
if (vec == null)
{
throw new GdxRuntimeException("testCustomVec2 is reporting null!");
}
if ((vec.x != 314159) || (vec.y != 21718))
{
throw new GdxRuntimeException("testCustomVec2 is not read correctly! Expected: " + new Vector2(314159,21718) + " Actual: " + vec);
}
if (string == null)
{
throw new GdxRuntimeException("testCustomString is reporting null!");
}
if (!string.equalsIgnoreCase("excelsior!"))
{
throw new GdxRuntimeException("testCustomString is not read correctly! Expected: Excelsior! Actual: " + string);
}
System.out.println("*** TESTING PASSED ***");
}
/**
* Creates an array of SimpleSpatial objects from RubeImages.
*
* @param scene2
*/
private void createSpatialsFromRubeImages(RubeScene scene)
{
Array<RubeImage> images = scene.getImages();
if ((images != null) && (images.size > 0))
{
mSpatials = new Array<SimpleSpatial>();
for (int i = 0; i < images.size; i++)
{
RubeImage image = images.get(i);
mTmp.set(image.width, image.height);
String textureFileName = "data/" + image.file;
Texture texture = mTextureMap.get(textureFileName);
if (texture == null)
{
texture = new Texture(textureFileName);
mTextureMap.put(textureFileName, texture);
}
SimpleSpatial spatial = new SimpleSpatial(texture, image.flip, image.body, image.color, mTmp, image.center,
image.angleInRads * MathUtils.radiansToDegrees);
mSpatials.add(spatial);
}
}
}
/**
* Creates an array of PolySpatials based on fixture information from the scene. Note that
* fixtures create aligned textures.
*
* @param scene
*/
private void createPolySpatialsFromRubeFixtures(RubeScene scene)
{
Array<Body> bodies = scene.getBodies();
EarClippingTriangulator ect = new EarClippingTriangulator();
if ((bodies != null) && (bodies.size > 0))
{
mPolySpatials = new Array<PolySpatial>();
Vector2 bodyPos = new Vector2();
// for each body in the scene...
for (int i = 0; i < bodies.size; i++)
{
Body body = bodies.get(i);
bodyPos.set(body.getPosition());
float bodyAngle = body.getAngle()*MathUtils.radiansToDegrees;
Array<Fixture> fixtures = body.getFixtureList();
if ((fixtures != null) && (fixtures.size > 0))
{
// for each fixture on the body...
for (int j = 0; j < fixtures.size; j++)
{
Fixture fixture = fixtures.get(j);
String textureName = (String)scene.getCustom(fixture, "TextureMask", null);
if (textureName != null)
{
String textureFileName = "data/" + textureName;
Texture texture = mTextureMap.get(textureFileName);
TextureRegion textureRegion = null;
if (texture == null)
{
texture = new Texture(textureFileName);
texture.setWrap(TextureWrap.Repeat, TextureWrap.Repeat);
mTextureMap.put(textureFileName, texture);
textureRegion = new TextureRegion(texture);
mTextureRegionMap.put(texture, textureRegion);
}
else
{
textureRegion = mTextureRegionMap.get(texture);
}
// only handle polygons at this point -- no chain, edge, or circle fixtures.
if (fixture.getType() == Shape.Type.Polygon)
{
PolygonShape shape = (PolygonShape) fixture.getShape();
int vertexCount = shape.getVertexCount();
float[] vertices = new float[vertexCount * 2];
// static bodies are texture aligned and do not get drawn based off of the related body.
if (body.getType() == BodyType.StaticBody)
{
for (int k = 0; k < vertexCount; k++)
{
shape.getVertex(k, mTmp);
mTmp.rotate(bodyAngle);
mTmp.add(bodyPos); // convert local coordinates to world coordinates to that textures are
// aligned
vertices[k * 2] = mTmp.x * PolySpatial.PIXELS_PER_METER;
vertices[k * 2 + 1] = mTmp.y * PolySpatial.PIXELS_PER_METER;
}
short [] triangleIndices = ect.computeTriangles(vertices).toArray();
PolygonRegion region = new PolygonRegion(textureRegion, vertices, triangleIndices);
PolySpatial spatial = new PolySpatial(region, Color.WHITE);
mPolySpatials.add(spatial);
}
else
{
// all other fixtures are aligned based on their associated body.
for (int k = 0; k < vertexCount; k++)
{
shape.getVertex(k, mTmp);
vertices[k * 2] = mTmp.x * PolySpatial.PIXELS_PER_METER;
vertices[k * 2 + 1] = mTmp.y * PolySpatial.PIXELS_PER_METER;
}
short [] triangleIndices = ect.computeTriangles(vertices).toArray();
PolygonRegion region = new PolygonRegion(textureRegion, vertices, triangleIndices);
PolySpatial spatial = new PolySpatial(region, body, Color.WHITE);
mPolySpatials.add(spatial);
}
}
else if (fixture.getType() == Shape.Type.Circle)
{
CircleShape shape = (CircleShape)fixture.getShape();
float radius = shape.getRadius();
int vertexCount = (int)(12f * radius);
float [] vertices = new float[vertexCount*2];
if (body.getType() == BodyType.StaticBody)
{
mTmp3.set(shape.getPosition());
for (int k = 0; k < vertexCount; k++)
{
// set the initial position
mTmp.set(radius,0);
// rotate it by 1/vertexCount * k
mTmp.rotate(360f*k/vertexCount);
// add it to the position.
mTmp.add(mTmp3);
mTmp.rotate(bodyAngle);
mTmp.add(bodyPos); // convert local coordinates to world coordinates so that textures are aligned
vertices[k*2] = mTmp.x*PolySpatial.PIXELS_PER_METER;
vertices[k*2+1] = mTmp.y*PolySpatial.PIXELS_PER_METER;
}
short [] triangleIndices = ect.computeTriangles(vertices).toArray();
PolygonRegion region = new PolygonRegion(textureRegion, vertices, triangleIndices);
PolySpatial spatial = new PolySpatial(region, Color.WHITE);
mPolySpatials.add(spatial);
}
else
{
mTmp3.set(shape.getPosition());
for (int k = 0; k < vertexCount; k++)
{
// set the initial position
mTmp.set(radius,0);
// rotate it by 1/vertexCount * k
mTmp.rotate(360f*k/vertexCount);
// add it to the position.
mTmp.add(mTmp3);
vertices[k*2] = mTmp.x*PolySpatial.PIXELS_PER_METER;
vertices[k*2+1] = mTmp.y*PolySpatial.PIXELS_PER_METER;
}
short [] triangleIndices = ect.computeTriangles(vertices).toArray();
PolygonRegion region = new PolygonRegion(textureRegion, vertices, triangleIndices);
PolySpatial spatial = new PolySpatial(region, body, Color.WHITE);
mPolySpatials.add(spatial);
}
}
}
}
}
}
}
}
@Override
public void resize(int width, int height)
{
}
@Override
public void pause()
{
}
@Override
public void resume()
{
}
@Override
public boolean keyDown(int keycode)
{
return false;
}
@Override
public boolean keyUp(int keycode)
{
return false;
}
@Override
public boolean keyTyped(char character)
{
return false;
}
@Override
public boolean touchDown(int screenX, int screenY, int pointer, int button)
{
mCamPos.set(screenX, screenY, 0);
mCam.unproject(mCamPos);
return true;
}
@Override
public boolean touchUp(int screenX, int screenY, int pointer, int button)
{
return false;
}
@Override
public boolean touchDragged(int screenX, int screenY, int pointer)
{
mCurrentPos.set(screenX, screenY, 0);
mCam.unproject(mCurrentPos);
mCam.position.sub(mCurrentPos.sub(mCamPos));
mCam.update();
return true;
}
@Override
public boolean mouseMoved(int screenX, int screenY)
{
return false;
}
@Override
public boolean scrolled(int amount)
{
mCam.zoom += (amount * 0.1f);
if (mCam.zoom < 0.1f)
{
mCam.zoom = 0.1f;
}
mCam.update();
return true;
}
@Override
public void beginContact(Contact contact)
{
}
@Override
public void endContact(Contact contact)
{
}
@Override
public void preSolve(Contact contact, Manifold oldManifold)
{
}
@Override
public void postSolve(Contact contact, ContactImpulse impulse)
{
}
}